/*- * See the file LICENSE for redistribution information. * * Copyright (c) 2002-2006 * Sleepycat Software. All rights reserved. * * $Id: LogManager.java,v 1.1 2006/05/06 09:00:02 ckaestne Exp $ */ package com.sleepycat.je.log; import java.io.IOException; import java.io.RandomAccessFile; import java.nio.BufferOverflowException; import java.nio.ByteBuffer; import java.nio.channels.ClosedChannelException; import java.util.List; import java.util.zip.Checksum; import com.sleepycat.je.DatabaseException; import com.sleepycat.je.EnvironmentStats; import com.sleepycat.je.RunRecoveryException; import com.sleepycat.je.StatsConfig; import com.sleepycat.je.cleaner.TrackedFileSummary; import com.sleepycat.je.cleaner.UtilizationTracker; import com.sleepycat.je.config.EnvironmentParams; import com.sleepycat.je.dbi.DbConfigManager; import com.sleepycat.je.dbi.EnvironmentImpl; import com.sleepycat.je.latch.Latch; import com.sleepycat.je.latch.LatchSupport; import com.sleepycat.je.log.entry.LogEntry; import com.sleepycat.je.utilint.Adler32; import com.sleepycat.je.utilint.DbLsn; import com.sleepycat.je.utilint.TestHook; import com.sleepycat.je.utilint.Tracer; /** * The LogManager supports reading and writing to the JE log. */ abstract public class LogManager { // no-op loggable object private static final String DEBUG_NAME = LogManager.class.getName(); /* * Log entry header field sizes */ static final int HEADER_BYTES = 14; // size of entry header static final int CHECKSUM_BYTES = 4; // size of checksum field static final int PREV_BYTES = 4; // size of previous field static final int HEADER_CONTENT_BYTES = HEADER_BYTES - CHECKSUM_BYTES; static final int HEADER_CHECKSUM_OFFSET = 0; static final int HEADER_ENTRY_TYPE_OFFSET = 4; static final int HEADER_VERSION_OFFSET = 5; static final int HEADER_PREV_OFFSET = 6; static final int HEADER_SIZE_OFFSET = 6+4; protected LogBufferPool logBufferPool; // log buffers protected Latch logWriteLatch; // synchronizes log writes private boolean doChecksumOnRead; // if true, do checksum on read private FileManager fileManager; // access to files private CheckpointMonitor checkpointMonitor; protected EnvironmentImpl envImpl; private boolean readOnly; private int readBufferSize; // how many bytes to read when faulting in. /* The last LSN in the log during recovery. */ private long lastLsnAtRecovery = DbLsn.NULL_LSN; /* Stats */ /* * Number of times we have to repeat a read when we fault in an object * because the initial read was too small. */ private int nRepeatFaultReads; /* * Number of times we have to use the temporary marshalling buffer to * write to the log. */ private long nTempBufferWrites; /* For unit tests */ private TestHook readHook; // used for generating exceptions on log reads /** * There is a single log manager per database environment. */ public LogManager(EnvironmentImpl envImpl, boolean readOnly) throws DatabaseException { // Set up log buffers this.envImpl = envImpl; this.fileManager = envImpl.getFileManager(); DbConfigManager configManager = envImpl.getConfigManager(); this.readOnly = readOnly; logBufferPool = new LogBufferPool(fileManager, envImpl); /* See if we're configured to do a checksum when reading in objects. */ doChecksumOnRead = configManager.getBoolean(EnvironmentParams.LOG_CHECKSUM_READ); logWriteLatch = LatchSupport.makeLatch(DEBUG_NAME, envImpl); readBufferSize = configManager.getInt(EnvironmentParams.LOG_FAULT_READ_SIZE); checkpointMonitor = new CheckpointMonitor(envImpl); } public boolean getChecksumOnRead() { return doChecksumOnRead; } public long getLastLsnAtRecovery() { return lastLsnAtRecovery; } public void setLastLsnAtRecovery(long lastLsnAtRecovery) { this.lastLsnAtRecovery = lastLsnAtRecovery; } /** * Reset the pool when the cache is resized. This method is called after * the memory budget has been calculated. */ public void resetPool(DbConfigManager configManager) throws DatabaseException { logBufferPool.reset(configManager); } /* * Writing to the log */ /** * Log this single object and force a write of the log files. * @param item object to be logged * @param fsyncRequired if true, log files should also be fsynced. * @return LSN of the new log entry */ public long logForceFlush(LoggableObject item, boolean fsyncRequired) throws DatabaseException { return log(item, false, // is provisional true, // flush required fsyncRequired, false, // forceNewLogFile DbLsn.NULL_LSN); // oldNodeLsn, for obsolete counting. } /** * Log this single object and force a flip of the log files. * @param item object to be logged * @param fsyncRequired if true, log files should also be fsynced. * @return LSN of the new log entry */ public long logForceFlip(LoggableObject item) throws DatabaseException { return log(item, false, // is provisional true, // flush required false, // fsync required true, // forceNewLogFile DbLsn.NULL_LSN); // oldNodeLsn, for obsolete counting. } /** * Write a log entry. * @return LSN of the new log entry */ public long log(LoggableObject item) throws DatabaseException { return log(item, false, // is provisional false, // flush required false, // fsync required false, // forceNewLogFile DbLsn.NULL_LSN); // old lsn } /** * Write a log entry. * @return LSN of the new log entry */ public long log(LoggableObject item, boolean isProvisional, long oldNodeLsn) throws DatabaseException { return log(item, isProvisional, false, // flush required false, // fsync required false, // forceNewLogFile oldNodeLsn); } /** * Write a log entry. * @param item is the item to be logged. * @param isProvisional true if this entry should not be read during * recovery. * @param flushRequired if true, write the log to the file after * adding the item. i.e. call java.nio.channel.FileChannel.write(). * @param fsyncRequired if true, fsync the last file after adding the item. * @param oldNodeLsn is the previous version of the node to be counted as * obsolete, or null if the item is not a node or has no old LSN. * @return LSN of the new log entry */ private long log(LoggableObject item, boolean isProvisional, boolean flushRequired, boolean fsyncRequired, boolean forceNewLogFile, long oldNodeLsn) throws DatabaseException { if (readOnly) { return DbLsn.NULL_LSN; } boolean marshallOutsideLatch = item.marshallOutsideWriteLatch(); ByteBuffer marshalledBuffer = null; UtilizationTracker tracker = envImpl.getUtilizationTracker(); LogResult logResult = null; try { /* * If possible, marshall this item outside the log write * latch to allow greater concurrency by shortening the * write critical section. */ if (marshallOutsideLatch) { int itemSize = item.getLogSize(); int entrySize = itemSize + HEADER_BYTES; marshalledBuffer = marshallIntoBuffer(item, itemSize, isProvisional, entrySize); } logResult = logItem(item, isProvisional, flushRequired, forceNewLogFile, oldNodeLsn, marshallOutsideLatch, marshalledBuffer, tracker); } catch (BufferOverflowException e) { /* * A BufferOverflowException may be seen when a thread is * interrupted in the middle of the log and the nio direct buffer * is mangled is some way by the NIO libraries. JE applications * should refrain from using thread interrupt as a thread * communications mechanism because nio behavior in the face of * interrupts is uncertain. See SR [#10463]. * * One way or another, this type of io exception leaves us in an * unworkable state, so throw a run recovery exception. */ throw new RunRecoveryException(envImpl, e); } catch (IOException e) { /* * Other IOExceptions, such as out of disk conditions, should * notify the application but leave the environment in workable * condition. */ throw new DatabaseException(Tracer.getStackTrace(e), e); } /* * Finish up business outside of the log write latch critical section. */ /* * If this logged object needs to be fsynced, do so now using the group * commit mechanism. */ if (fsyncRequired) { fileManager.groupSync(); } /* * Periodically, as a function of how much data is written, ask the * checkpointer or the cleaner to wake up. */ if (logResult.wakeupCheckpointer) { checkpointMonitor.activate(); } if (logResult.wakeupCleaner) { tracker.activateCleaner(); } return logResult.currentLsn; } abstract protected LogResult logItem(LoggableObject item, boolean isProvisional, boolean flushRequired, boolean forceNewLogFile, long oldNodeLsn, boolean marshallOutsideLatch, ByteBuffer marshalledBuffer, UtilizationTracker tracker) throws IOException, DatabaseException; /** * Called within the log write critical section. */ protected LogResult logInternal(LoggableObject item, boolean isProvisional, boolean flushRequired, boolean forceNewLogFile, long oldNodeLsn, boolean marshallOutsideLatch, ByteBuffer marshalledBuffer, UtilizationTracker tracker) throws IOException, DatabaseException { /* * Do obsolete tracking before marshalling a FileSummaryLN into the log * buffer so that a FileSummaryLN counts itself. countObsoleteNode * must be called before computing the entry size, since it can change * the size of a FileSummaryLN entry that we're logging */ LogEntryType entryType = item.getLogType(); if (oldNodeLsn != DbLsn.NULL_LSN) { tracker.countObsoleteNode(oldNodeLsn, entryType); } /* * If an item must be protected within the log write latch for * marshalling, take care to also calculate its size in the protected * section. Note that we have to get the size *before* marshalling so * that the currentLsn and size are correct for utilization tracking. */ int entrySize; if (marshallOutsideLatch) { entrySize = marshalledBuffer.limit(); } else { entrySize = item.getLogSize() + HEADER_BYTES; } /* * Get the next free slot in the log, under the log write latch. Bump * the LSN values, which gives us a valid previous pointer, which is * part of the log entry header. That's why doing the checksum must be * in the log write latch -- we need to bump the LSN first, and bumping * the LSN must be done within the log write latch. */ if (forceNewLogFile) { fileManager.forceNewLogFile(); } boolean flippedFile = fileManager.bumpLsn(entrySize); long currentLsn = DbLsn.NULL_LSN; boolean wakeupCleaner = false; boolean usedTemporaryBuffer = false; try { currentLsn = fileManager.getLastUsedLsn(); /* * countNewLogEntry and countObsoleteNodeInexact cannot change a * FileSummaryLN size, so they are safe to call after getLogSize(). */ wakeupCleaner = tracker.countNewLogEntry(currentLsn, entryType, entrySize); /* * LN deletions are obsolete immediately. Inexact counting is used * to save resources because the cleaner knows that all deleted LNs * are obsolete. */ if (item.countAsObsoleteWhenLogged()) { tracker.countObsoleteNodeInexact(currentLsn, entryType); } /* * This item must be marshalled within the log write latch. */ if (!marshallOutsideLatch) { marshalledBuffer = marshallIntoBuffer(item, entrySize-HEADER_BYTES, isProvisional, entrySize); } /* Sanity check */ if (entrySize != marshalledBuffer.limit()) { throw new DatabaseException( "Logged item entrySize= " + entrySize + " but marshalledSize=" + marshalledBuffer.limit() + " type=" + entryType + " currentLsn=" + DbLsn.getNoFormatString(currentLsn)); } /* * Ask for a log buffer suitable for holding this new entry. If * the current log buffer is full, or if we flipped into a new * file, write it to disk and get a new, empty log buffer to * use. The returned buffer will be latched for write. */ LogBuffer useLogBuffer = logBufferPool.getWriteBuffer(entrySize, flippedFile); /* Add checksum to entry. */ marshalledBuffer = addPrevOffsetAndChecksum(marshalledBuffer, fileManager.getPrevEntryOffset(), entrySize); /* * If the LogBufferPool buffer (useBuffer) doesn't have sufficient * space (since they're fixed size), just use the temporary buffer * and throw it away when we're done. That way we don't grow the * LogBuffers in the pool permanently. We risk an OOME on this * temporary usage, but we'll risk it. [#12674] */ useLogBuffer.latchForWrite(); try { ByteBuffer useBuffer = useLogBuffer.getDataBuffer(); if (useBuffer.capacity() - useBuffer.position() < entrySize) { fileManager.writeLogBuffer (new LogBuffer(marshalledBuffer, currentLsn)); usedTemporaryBuffer = true; assert useBuffer.position() == 0; nTempBufferWrites++; } else { /* Copy marshalled object into write buffer. */ useBuffer.put(marshalledBuffer); } } finally { useLogBuffer.release(); } } catch (Exception e) { /* * The LSN pointer, log buffer position, and corresponding file * position march in lockstep. * * 1. We bump the LSN. * 2. We copy loggable item into the log buffer. * 3. We may try to write the log buffer. * * If we've failed to put the item into the log buffer (2), we need * to restore old LSN state so that the log buffer doesn't have a * hole. [SR #12638] If we fail after (2), we don't need to restore * state, because log buffers will still match file positions. */ fileManager.restoreLastPosition(); if (e instanceof DatabaseException) { throw (DatabaseException) e; } else if (e instanceof IOException){ throw (IOException) e; } else { throw new DatabaseException(e); } } /* * Tell the log buffer pool that we finished the write. Record the * LSN against this logbuffer, and write the buffer to disk if * needed. */ if (!usedTemporaryBuffer) { logBufferPool.writeCompleted(currentLsn, flushRequired); } /* * If the txn is not null, the first item is an LN. Update the txn with * info about the latest LSN. Note that this has to happen within the * log write latch. */ item.postLogWork(currentLsn); boolean wakeupCheckpointer = checkpointMonitor.recordLogWrite(entrySize, item); return new LogResult(currentLsn, wakeupCheckpointer, wakeupCleaner); } /** * Serialize a loggable object into this buffer. */ private ByteBuffer marshallIntoBuffer(LoggableObject item, int itemSize, boolean isProvisional, int entrySize) throws DatabaseException { ByteBuffer destBuffer = ByteBuffer.allocate(entrySize); /* Reserve 4 bytes at the head for the checksum. */ destBuffer.position(CHECKSUM_BYTES); /* Write the header. */ writeHeader(destBuffer, item.getLogType(), itemSize, isProvisional); /* Put the entry in. */ item.writeToLog(destBuffer); /* Set the limit so it can be used as the size of the entry. */ destBuffer.flip(); return destBuffer; } private ByteBuffer addPrevOffsetAndChecksum(ByteBuffer destBuffer, long lastOffset, int entrySize) { Checksum checksum = Adler32.makeChecksum(); /* Add the prev pointer */ destBuffer.position(HEADER_PREV_OFFSET); LogUtils.writeUnsignedInt(destBuffer, lastOffset); /* Now calculate the checksum and write it into the buffer. */ checksum.update(destBuffer.array(), CHECKSUM_BYTES, (entrySize - CHECKSUM_BYTES)); destBuffer.position(0); LogUtils.writeUnsignedInt(destBuffer, checksum.getValue()); /* Leave this buffer ready for copying into another buffer. */ destBuffer.position(0); return destBuffer; } /** * Serialize a loggable object into this buffer. Return it ready for a * copy. */ ByteBuffer putIntoBuffer(LoggableObject item, int itemSize, long prevLogEntryOffset, boolean isProvisional, int entrySize) throws DatabaseException { ByteBuffer destBuffer = marshallIntoBuffer(item, itemSize, isProvisional, entrySize); return addPrevOffsetAndChecksum(destBuffer, 0, entrySize); } /** * Helper to write the common entry header. * @param destBuffer destination * @param item object being logged * @param itemSize We could ask the item for this, but are passing it * as a parameter for efficiency, because it's already available */ private void writeHeader(ByteBuffer destBuffer, LogEntryType itemType, int itemSize, boolean isProvisional) { // log entry type byte typeNum = itemType.getTypeNum(); destBuffer.put(typeNum); // version byte version = itemType.getVersion(); if (isProvisional) version = LogEntryType.setProvisional(version); destBuffer.put(version); // entry size destBuffer.position(HEADER_SIZE_OFFSET); LogUtils.writeInt(destBuffer, itemSize); } /* * Reading from the log. */ /** * Instantiate all the objects in the log entry at this LSN. * @param lsn location of entry in log. * @return log entry that embodies all the objects in the log entry. */ public LogEntry getLogEntry(long lsn) throws DatabaseException { /* * Fail loudly if the environment is invalid. A RunRecoveryException * must have occurred. */ envImpl.checkIfInvalid(); /* * Get a log source for the log entry which provides an abstraction * that hides whether the entry is in a buffer or on disk. Will * register as a reader for the buffer or the file, which will take a * latch if necessary. */ LogSource logSource = getLogSource(lsn); /* Read the log entry from the log source. */ return getLogEntryFromLogSource(lsn, logSource); } LogEntry getLogEntry(long lsn, RandomAccessFile file) throws DatabaseException { return getLogEntryFromLogSource (lsn, new FileSource(file, readBufferSize, fileManager)); } /** * Instantiate all the objects in the log entry at this LSN. This will * release the log source at the first opportunity. * * @param lsn location of entry in log * @return log entry that embodies all the objects in the log entry */ private LogEntry getLogEntryFromLogSource(long lsn, LogSource logSource) throws DatabaseException { try { /* * Read the log entry header into a byte buffer. Be sure to read it * in the order that it was written, and with the same marshalling! * Ideally, entry header read/write would be encapsulated in a * single class, but we don't want to have to instantiate a new * object in the critical path here. * XXX - false economy, change. */ long fileOffset = DbLsn.getFileOffset(lsn); ByteBuffer entryBuffer = logSource.getBytes(fileOffset); /* Read the checksum to move the buffer forward. */ ChecksumValidator validator = null; long storedChecksum = LogUtils.getUnsignedInt(entryBuffer); if (doChecksumOnRead) { validator = new ChecksumValidator(); validator.update(envImpl, entryBuffer, HEADER_CONTENT_BYTES, false); } /* Read the header. */ byte loggableType = entryBuffer.get(); // log entry type byte version = entryBuffer.get(); // version /* Read the size, skipping over the prev offset. */ entryBuffer.position(entryBuffer.position() + PREV_BYTES); int itemSize = LogUtils.readInt(entryBuffer); /* * Now that we know the size, read the rest of the entry * if the first read didn't get enough. */ if (entryBuffer.remaining() < itemSize) { entryBuffer = logSource.getBytes(fileOffset + HEADER_BYTES, itemSize); nRepeatFaultReads++; } /* * Do entry validation. Run checksum before checking the entry * type, it will be the more encompassing error. */ if (doChecksumOnRead) { /* Check the checksum first. */ validator.update(envImpl, entryBuffer, itemSize, false); validator.validate(envImpl, storedChecksum, lsn); } assert LogEntryType.isValidType(loggableType): "Read non-valid log entry type: " + loggableType; /* Read the entry. */ LogEntry logEntry = LogEntryType.findType(loggableType, version).getNewLogEntry(); logEntry.readEntry(entryBuffer, itemSize, version, true); /* For testing only; generate a read io exception. */ if (readHook != null) { readHook.doIOHook(); } /* * Done with the log source, release in the finally clause. Note * that the buffer we get back from logSource is just a duplicated * buffer, where the position and state are copied but not the * actual data. So we must not release the logSource until we are * done marshalling the data from the buffer into the object * itself. */ return logEntry; } catch (DatabaseException e) { /* * Propagate DatabaseExceptions, we want to preserve any subtypes * for downstream handling. */ throw e; } catch (ClosedChannelException e) { /* * The channel should never be closed. It may be closed because * of an interrupt received by another thread. See SR [#10463] */ throw new RunRecoveryException(envImpl, "Channel closed, may be "+ "due to thread interrupt", e); } catch (Exception e) { throw new DatabaseException(e); } finally { if (logSource != null) { logSource.release(); } } } /** * Fault in the first object in the log entry log entry at this LSN. * @param lsn location of object in log * @return the object in the log */ public Object get(long lsn) throws DatabaseException { LogEntry entry = getLogEntry(lsn); return entry.getMainItem(); } /** * Find the LSN, whether in a file or still in the log buffers. */ private LogSource getLogSource(long lsn) throws DatabaseException { /* * First look in log to see if this LSN is still in memory. */ LogBuffer logBuffer = logBufferPool.getReadBuffer(lsn); if (logBuffer == null) { try { /* Not in the in-memory log -- read it off disk. */ return new FileHandleSource (fileManager.getFileHandle(DbLsn.getFileNumber(lsn)), readBufferSize, fileManager); } catch (LogFileNotFoundException e) { /* Add LSN to exception message. */ throw new LogFileNotFoundException (DbLsn.getNoFormatString(lsn) + ' ' + e.getMessage()); } } else { return logBuffer; } } /** * Flush all log entries, fsync the log file. */ public void flush() throws DatabaseException { if (readOnly) { return; } flushInternal(); fileManager.syncLogEnd(); } abstract protected void flushInternal() throws LogException, DatabaseException; public void loadStats(StatsConfig config, EnvironmentStats stats) throws DatabaseException { stats.setNRepeatFaultReads(nRepeatFaultReads); stats.setNTempBufferWrites(nTempBufferWrites); if (config.getClear()) { nRepeatFaultReads = 0; nTempBufferWrites = 0; } logBufferPool.loadStats(config, stats); fileManager.loadStats(config, stats); } /** * Returns a tracked summary for the given file which will not be flushed. * Used for watching changes that occur while a file is being cleaned. */ abstract public TrackedFileSummary getUnflushableTrackedSummary(long file) throws DatabaseException; protected TrackedFileSummary getUnflushableTrackedSummaryInternal(long file) throws DatabaseException { return envImpl.getUtilizationTracker(). getUnflushableTrackedSummary(file); } /** * Count node as obsolete under the log write latch. This is done here * because the log write latch is managed here, and all utilization * counting must be performed under the log write latch. */ abstract public void countObsoleteNode(long lsn, LogEntryType type) throws DatabaseException; protected void countObsoleteNodeInternal(UtilizationTracker tracker, long lsn, LogEntryType type) throws DatabaseException { tracker.countObsoleteNode(lsn, type); } /** * Counts file summary info under the log write latch. */ abstract public void countObsoleteNodes(TrackedFileSummary[] summaries) throws DatabaseException; protected void countObsoleteNodesInternal(UtilizationTracker tracker, TrackedFileSummary[] summaries) throws DatabaseException { for (int i = 0; i < summaries.length; i += 1) { TrackedFileSummary summary = summaries[i]; tracker.addSummary(summary.getFileNumber(), summary); } } /** * Counts the given obsolete IN LSNs under the log write latch. */ abstract public void countObsoleteINs(List lsnList) throws DatabaseException; protected void countObsoleteINsInternal(List lsnList) throws DatabaseException { UtilizationTracker tracker = envImpl.getUtilizationTracker(); for (int i = 0; i < lsnList.size(); i += 1) { Long offset = (Long) lsnList.get(i); tracker.countObsoleteNode(offset.longValue(), LogEntryType.LOG_IN); } } /* For unit testing only. */ public void setReadHook(TestHook hook) { readHook = hook; } /** * LogResult holds the multivalue return from logInternal. */ static class LogResult { long currentLsn; boolean wakeupCheckpointer; boolean wakeupCleaner; LogResult(long currentLsn, boolean wakeupCheckpointer, boolean wakeupCleaner) { this.currentLsn = currentLsn; this.wakeupCheckpointer = wakeupCheckpointer; this.wakeupCleaner = wakeupCleaner; } } }